Parallel Firewall
Objectives
- Learn how to design and implement parallel programs
- Get experienced at utilizing the POSIX threading API
- Learn how to convert a serial program into a parallel one
Statement
A firewall is a program that checks network packets against a series of filters which provide a decision regarding dropping or allowing the packets to continue to their intended destination.
In a real setup, the network card will receive real packets (e.g. packets having Ethernet
, IP
headers plus payload) from the network and will send them to the firewall for processing.
The firewall will decide if the packets are to be dropped or not and, if not, passes them further.
In this assignment, instead of real network packets, we'll deal with made up packets consisting of a made up source (a number), a made up destination (also a number), a timestamp (also a number) and some payload. And instead of the network card providing the packets, we'll have a producer thread creating these packets.
The created packets will be inserted into a circular buffer
, out of which consumer threads (which implement the firewall logic) will take packets and process them in order to decide whether they advance to the destination.
The result of this processing is a log file in which the firewall will record the decision taken (PASS or DROP) for each packet, along with other information such as timestamp.
The purpose of this assignment is to:
implement the circular buffer, along with synchronization mechanisms for it to work in a multithreaded program
implement the consumer threads, which consume packets and process them
provide the log file containing the result of the packet processing
Support Code
The support code consists of the directories:
src/
contains the skeleton for the parallelized firewall and the already implemented serial code insrc/serial.c
. You will have to implement the missing parts marked asTODO
utils/
contains utility files used for debugging and logging.tests/
contains tests used to validate and grade the assignment.
Implementation
Firewall Threads
In order to parallelize the firewall we have to distribute the packets to multiple threads.
The packets will be added to a shared data structure (visible to all threads) by a producer
thread and processed by multiple consumer
threads.
Each consumer
thread picks a packet from the shared data structure, checks it against the filter function and writes the packet hash together with the drop/accept decision to a log file.
consumer
threads stop waiting for new packets from the producer
thread and exit when the producer
thread closes the connection to the shared data structure.
The consumer
threads must not do any form of busy waiting.
When there are new packets that need to be handled, the consumer
threads must be notified.
Waiting in a while()
loop or sleeping is not considered a valid synchronization mechanism and points will be deducted.
Implement the consumer
related functions marked with TODO
in the src/consumer.c
file.
The number of consumer threads will be passed as the 3rd command-line argument
Ring Buffers
A ring buffer (or a circular buffer) is a data structure that stores its elements in a circular fixed size array. One of the advantages of using such a data structure as opposed to an array is that it acts as a FIFO, without the overhead of moving the elements to the left as they are consumed. Thus, the shared ring buffer offers the following fields:
write_pos
index in the buffer used by theproducer
thread for appending new packets.read_pos
index in the buffer used by theconsumer
threads to pick packets.cap
the size of the internal buffer.data
pointer to the internal buffer.
Apart from these fields you have to add synchronization primitives in order to allow multiple threads to access the ring buffer in a deterministic manner.
You can use mutexes, semaphores, conditional variables and other synchronization mechanisms offered by the pthread
library.
You will have to implement the following interface for the ring buffer:
ring_buffer_init()
: initialize the ring buffer (allocate memory and synchronization primitives).ring_buffer_enqueue()
: add elements to the ring buffer.ring_buffer_dequeue()
: remove elements from the ring buffer.ring_buffer_destroy()
: free up the memory used by the ring_buffer.ring_buffer_stop()
: finish up using the ring buffer for the calling thread.
Log File
The output of the firewall will be a log file with the rows containing the firewall's decision, the hash of the packet and its timestamp.
The actual format can be found in the serial implementation (at src/serial.c
).
When processing the packets in parallel the threads will finish up the work in a non deterministic order.
The packet processing functions are already implemented in src/packet.c
We would like the logs to be sorted by the packet timestamp, the order that they came in from the producer.
Thus, the consumers
should insert the packet information to the log file such as the result is ordered by timestamp.
The printing format can be found in ./src/serial.c
The logs must be written to the file in ascending order during packet processing. Sorting the log file after the consumer threads have finished processing is not considered a valid synchronization mechanism and points will be deducted.
Operations
Building
To build both the serial and the parallel versions, run make
in the src/
directory:
student@so:~/.../content/assignments/parallel-firewall$ cd src/
student@so:~/.../assignments/parallel-firewall/src$ make
That will create the serial
and firewall
binaries.
Testing and Grading
Testing is automated.
Tests are located in the tests/
directory.
To test and grade your assignment solution, enter the tests/
directory and run grade.sh
.
student@so:~/.../content/assignments/parallel-firewall$ cd tests/
student@so:~/.../content/assignments/parallel-firewall/tests$ ./grade.sh
Note that this requires linters being available. The easiest way to test the project is to use a Docker-based setup with everything installed and configured (see the README.checker.md file for instructions).
To create the tests, run:
student@so:~/.../content/assignments/parallel-firewall/tests$ make check
To remove the tests, run:
student@so:~/.../content/assignments/parallel-firewall/tests$ make distclean
When using grade.sh
you will get a maximum of 90/100 points for general correctness and a maximum of 10/100 points for coding style.
Restrictions
- Threads must yield the cpu when waiting for empty/full buffers i.e. not doing
busy waiting
. - The logs must be written as they are processed and not after the processing is done, in ascending order by the timestamp.
- The number of running threads must be at least
num_consumers + 1
, wherenum_consumers
is the 3rd command-line argument of thefirewall
binary.
Grades
- 10 points are awarded for a single consumer solution that also implements the ring buffer
- 50 points are awarded for a multi consumer solution
- 30 points are awarded for a multi consumer solution that writes the logs in the sorted manner (bearing in mind the above restrictions)
Running the Checker
Each test is worth a number of points.
The maximum grade is 90
.
A successful run will show the output:
student@so:~/.../assignments/parallel-firewall/tests$ make check
[...]
Test [ 10 packets, sort False, 1 thread ] ...................... passed ... 3
Test [ 1,000 packets, sort False, 1 thread ] ...................... passed ... 3
Test [20,000 packets, sort False, 1 thread ] ...................... passed ... 4
Test [ 10 packets, sort True , 2 threads] ...................... passed ... 5
Test [ 10 packets, sort True , 4 threads] ...................... passed ... 5
Test [ 100 packets, sort True , 2 threads] ...................... passed ... 5
Test [ 100 packets, sort True , 4 threads] ...................... passed ... 5
Test [ 1,000 packets, sort True , 2 threads] ...................... passed ... 5
Test [ 1,000 packets, sort True , 4 threads] ...................... passed ... 5
Test [10,000 packets, sort True , 2 threads] ...................... passed ... 5
Test [10,000 packets, sort True , 4 threads] ...................... passed ... 5
Test [20,000 packets, sort True , 2 threads] ...................... passed ... 5
Test [20,000 packets, sort True , 4 threads] ...................... passed ... 5
Test [ 1,000 packets, sort False, 4 threads] ...................... passed ... 5
Test [ 1,000 packets, sort False, 8 threads] ...................... passed ... 5
Test [10,000 packets, sort False, 4 threads] ...................... passed ... 5
Test [10,000 packets, sort False, 8 threads] ...................... passed ... 5
Test [20,000 packets, sort False, 4 threads] ...................... passed ... 5
Test [20,000 packets, sort False, 8 threads] ...................... passed ... 5
Checker: 90/100
Running the Linters
To run the linters, use the make lint
command in the tests/
directory:
student@so:~/.../assignments/parallel-firewall/tests$ make lint
[...]
cd .. && checkpatch.pl -f checker/*.sh tests/*.sh
[...]
cd .. && cpplint --recursive src/ tests/ checker/
[...]
cd .. && shellcheck checker/*.sh tests/*.sh
Note that the linters have to be installed on your system: checkpatch.pl
, cpplint
, shellcheck
.
They also need to have certain configuration options.
It's easiest to run them in a Docker-based setup with everything configured.
Fine-Grained Testing
Input tests cases are located in tests/in/
and are generated by the checker.
The expected results are generated by the checker while running the serial implementation.
If you want to run a single test, use the below commands while in the src/
directory:
student@so:~/.../assignments/parallel-firewall/src$ ./firewall ../tests/in/test_<num_packets>.in <output_file> <number_of_consumers>
Results provided by the serial and parallel implementation must be the same for the test to successfully pass.